﻿/*
*   QFocuser 0.95 - class for keyboard navigable AJAX widgets for better usability and accessibility
*
*	Features:
*		- allow to your widget listen key events when its focused
*		- focus can be enabled on any element
*		- fires focus and blur events (so your table row will NOT remain highlighted after click out of table for example)
*		- make your widget to be accessible by tab key
*		- tiny and library agnostic
*		- this implementation could serve as a model for the others
*		- works also in Safari!
*
*	Tested on: IE6/7, FF2/3, Safari, Webkit, Opera and Chrome
*	
*   Key navigation is important part in overall usability and accessibility of all applications.
*	If you have only one widget, key events can be safely attached to document, but if more than one widgets
*	are involved, each widget has to react to keyboard separately, which means when it has focus.
*	Also, each widget has to react when focus is lost (remove highlighted row for example).
*	There is no another solution than listen blur event. Any other solution based e.g. on mouseclick will fail with iframes.
*
*	.. read this: http://dojotoolkit.org/book/dojo-book-0-9/part-3-programmatic-dijit-and-dojo/writing-your-own-widget-class/creating-accessi
*
*	How it should work: 
*		Key tab switches between your widgets, in order defined by tabIndex. Focused widget can listen key events.
*
*	How it work: 
*		To make elements focusable, set them tabindex. That will allow you to attach keyboard events to them too.
*		The tabindex value can allow for some interesting behavior. If given a value of "-1", the element can't be tabbed 
*		to but focus can be given to the element programmatically (using element.focus()).
*		If given a value of 0, the element can be focused via the keyboard and falls into the tabbing flow of the document.
*		Values greater than 0 create a priority level with 1 being the most important.
*
*	Safari issue:
*		Current version of safari doesn't support tabIndex for regular elements. WebKit nightly build does.
*		This library has a workaround for it, to have almost same behavior.
*
*	Hiding the Browsers Focus Borders issue:
*		All browser including IE6/7 will show pesky dotted borders around focused elements for accessibility reasons.
*		The dotted visual clutters up the design. To remove them, use options doNotShowBrowserFocusDottedBorder.
*		It has to be done in code, because Internet Explorer has no style property for that. If you are remove them, do not
*		forget use own focused element highlighting.
*	
*	Example:
*
*		var focuser = new QFocuser(widgetContainer, {
*			onFocus: function(focusedEl) { .. add highlighted class or whatever }
*			onBlur: function(focusedEl) { .. remove highlighted class or whatever }
*			onKeydown: e.g. handleArrows...
*		});
*		// attach your own keys listeners
*		$(focuser.getEl()).addEvent('keydown', e.g. handleArrows);
*		// when your widget decided to set the focus (and then receive key events)
*		focuser.focus(tableRowForExample);
*
*	Links:
*
*		http://snook.ca/archives/accessibility_and_usability/elements_focusable_with_tabindex/
*		http://wiki.codetalks.org/wiki/index.php/Docs/Keyboard_navigable_JS_widgets
*		http://dev.aol.com/dhtml_style_guide - recommended key shortcuts
*		http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
*
*	License: MIT-style license. Copyright: Copyright (c) 2009 Daniel Steigerwald, daniel.steigerwald.cz
*
*/

var QFocuser = (function() {

	// current safari doesnt support tabindex for elements, but chrome does. 
	// When Safari nightly version become current, this switch will be removed.
	var supportTabIndexOnRegularElements = (function() {
		var webKitFields = RegExp("( AppleWebKit/)([^ ]+)").exec(navigator.userAgent);
		if (!webKitFields || webKitFields.length < 3) return true; // every other browser support it
		var versionString = webKitFields[2],
                    isNightlyBuild = versionString.indexOf("+") != -1;
		if (isNightlyBuild || (/chrome/i).test(navigator.userAgent)) return true;
	})();

	return (supportTabIndexOnRegularElements ? function(widget, options) {

		var isIE = document.attachEvent && !document.addEventListener,
			focused,
			previousFocused,
			lastState,
			widgetState,
			widgetFocusBlurTimer;

		options = (function() {
			var defaultOptions = {
				onFocus: function(el, e) { },
				onBlur: function(el, e) { },
				onWidgetFocus: function() { },
				onWidgetBlur: function() { },
				tabIndex: 0, // add tabindex to your widget to be attainable by tab key
				doNotShowBrowserFocusDottedBorder: true
			};
			for (var option in options) defaultOptions[option] = options[option];
			return defaultOptions;
		})();

		init();

		// something to make IE happy
		if (isIE) {
			window.attachEvent('onunload', function() {
				window.detachEvent('onunload', arguments.callee);
				widget.clearAttributes();
			});
		}

		function init() {
			setTabIndex(widget, 0);
			// IE remembers focus after page reload but don't fire focus
			if (isIE && widget == widget.ownerDocument.activeElement) widget.blur();
			toggleEvents(true);
		};

		function hasTabIndex(el) {
			var attr = el.getAttributeNode('tabindex');
			return attr && attr.specified;
		};

		function setTabIndex(el, number) {
			var test = document.createElement('div');
			test.setAttribute('tabindex', 123);
			var prop = hasTabIndex(test) ? 'tabindex' : 'tabIndex';
			(setTabIndex = function(el, number) {
				el.setAttribute(prop, '' + number);
				if (options.doNotShowBrowserFocusDottedBorder) hideFocusBorder(el);
			})(el, number);
		};

		function getTabIndex(el) {
			return hasTabIndex(el) && el.tabIndex;
		};

		function hideFocusBorder(el) {
			if (isIE) el.hideFocus = true;
			else el.style.outline = 0;
		};

		function toggleEvents(register) {
			var method = register ? isIE ? 'attachEvent' : 'addEventListener' : isIE ? 'detachEvent' : 'removeEventListener';
			if (isIE) {
				widget[method]('onfocusin', onFocusBlur);
				widget[method]('onfocusout', onFocusBlur);
			}
			else {
				widget[method]('focus', onFocusBlur, true);
				widget[method]('blur', onFocusBlur, true);
			}
		};

		function onFocusBlur(e) {
			e = e || widget.ownerDocument.parentWindow.event;
			var target = e.target || e.srcElement;
			lastState = { focusin: 'Focus', focus: 'Focus', focusout: 'Blur', blur: 'Blur'}[e.type];
			// filter bubling focus and blur events, only these which come from elements setted by focus method are accepted		
			if (target == focused || target == previousFocused) {
				options['on' + lastState](target, e);
			}
			clearTimeout(widgetFocusBlurTimer);
			widgetFocusBlurTimer = setTimeout(onWidgetFocusBlur, 10);
		};

		function onWidgetFocusBlur() {
			if (widgetState == lastState) return;
			widgetState = lastState;
			options['onWidget' + widgetState]();
		};

		// call this method only for mousedown, in case of mouse is involved (keys are ok)
		function focus(el) {
			if (focused) {
				setTabIndex(focused, -1); // to disable tab walking in widget
				previousFocused = focused;
			}
			else setTabIndex(widget, -1);
			focused = el;
			setTabIndex(focused, 0);
			focused.focus();
		};

		// call this method after updating widget content, to be sure that tab will be attainable by tag key
		function refresh() {
			var setIndex = getTabIndex(widget) == -1,
				deleteFocused = true,
				els = widget.getElementsByTagName('*');
			for (var i = els.length; i--; ) {
				var idx = getTabIndex(els[i]);
				if (idx !== false && idx >= 0) setIndex = true;
				if (els[i] === focused) deleteFocused = false;
			}
			if (setIndex) setTabIndex(widget, 0);
			if (deleteFocused) focused = null;
		};

		function getFocused() {
			return focused;
		};

		// return element on which you should register key listeners
		function getKeyListener() {
			return widget;
		};

		function destroy() {
			toggleEvents();
		};

		return {
			focus: focus,
			getFocused: getFocused,
			getKeyListener: getKeyListener,
			refresh: refresh,
			destroy: destroy
		}
	} :

	// version for Safari, it mimics focus blur behaviour
	function(widget, options) {

		var focuser,
			lastState,
			widgetState = 'Blur',
			widgetFocusBlurTimer,
			focused;

		options = (function() {
			var defaultOptions = {
				onFocus: function(el, e) { },
				onBlur: function(el, e) { },
				onWidgetFocus: function() { },
				onWidgetBlur: function() { },
				tabIndex: 0, // add tabindex to your widget to be attainable by tab key
				doNotShowBrowserFocusDottedBorder: true
			};
			for (var option in options) defaultOptions[option] = options[option];
			return defaultOptions;
		})();

		init();

		function init() {
			focuser = widget.ownerDocument.createElement('input');
			var wrapper = widget.ownerDocument.createElement('span');
			wrapper.style.cssText = 'position: absolute; overflow: hidden; width: 0; height: 0';
			wrapper.appendChild(focuser);
			// it's placed in to widget, to mimics tabindex zero behaviour, where element document order matter 
			widget.insertBefore(wrapper, widget.firstChild);
			toggleEvent(true);
		};

		function toggleEvent(register) {
			var method = register ? 'addEventListener' : 'removeEventListener';
			focuser[method]('focus', onFocusBlur);
			focuser[method]('blur', onFocusBlur);
			window[method]('blur', onWindowBlur);
			widget[method]('mousedown', onWidgetMousedown);
		};

		// set active simulation
		function onWidgetMousedown(e) {
			if (widgetState == 'Blur') {
				setTimeout(function() {
					focuser.focus();
				}, 1);
			}
		};

		function onFocusBlur(e) {
			lastState = e.type.charAt(0).toUpperCase() + e.type.substring(1);
			if (focused) options['on' + lastState](focused, e);
			clearTimeout(widgetFocusBlurTimer);
			widgetFocusBlurTimer = setTimeout(onWidgetFocusBlur, 10);
		};

		function onWidgetFocusBlur() {
			if (widgetState == lastState) return;
			widgetState = lastState;
			options['onWidget' + widgetState]();
		};

		// safari is so stupid.. doesn't fire blur event when another browser tab is switched
		function onWindowBlur() {
			focuser.blur();
		};

		function focus(el) {
			setTimeout(function() {
				focuser.blur();
				setTimeout(function() {
					focused = el;
					focuser.focus();
				}, 10);
			}, 10);
		};

		function refresh() {
			var deleteFocused = true,
				els = widget.getElementsByTagName('*');
			for (var i = els.length; i--; ) {
				if (els[i] === focused) deleteFocused = false;
			}
			if (deleteFocused) focused = null;
		};

		function getFocused() {
			return focused;
		};

		function getKeyListener() {
			return focuser;
		};

		function destroy() {
			toggleEvents();
		};

		return {
			focus: focus,
			getFocused: getFocused,
			getKeyListener: getKeyListener,
			refresh: refresh,
			destroy: destroy
		}

	});

})();